﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Xml;
using System.Xml.Linq;
using System.Dynamic;
using System.Runtime.CompilerServices;
using System.Reflection;
using System.Text.RegularExpressions;

namespace DLR05
{
	public class XmlToExpression
	{
		#region Constants

		private const string SYMBOL_FORMAT = "[A-Za-z_][A-Za-z_0-9]";

		#endregion

		#region Variables

		private static Stack<Dictionary<string, ParameterExpression>> _symbolScope = new Stack<Dictionary<string, ParameterExpression>>();
		private static Stack<Dictionary<string, LabelTarget>> _labelScope = new Stack<Dictionary<string, LabelTarget>>();

		#endregion

		public static Expression Translate(XElement node)
		{
			if (node.Name == "Lambda")
			{
				return VisitLambda(node);
			}
			else if (node.Name == "Parameter")
			{
				return VisitParameter(node);
			}
			else if (node.Name == "Block")
			{
				return VisitBlock(node);
			}
			else if (node.Name == "Assign")
			{
				return VisitAssign(node);
			}
			else if (node.Name == "Constant")
			{
				return VisitConstant(node);
			}
			else if (node.Name == "Loop")
			{
				return VisitLoop(node);
			}
			else if (node.Name == "Conditional")
			{
				return VisitConditional(node);
			}
			else if (node.Name == "GreaterThan")
			{
				return VisitGreaterThan(node);
			}
			else if (node.Name == "MultiplyAssign")
			{
				return VisitMultiplyAssign(node);
			}
			else if (node.Name == "PostDecrementAssign")
			{
				return VisitPostDecrementAssign(node);
			}
			else if (node.Name == "Goto")
			{
				return VisitGoto(node);
			}
			else if (node.Name == "MemberAccess")
			{
				return VisitMemberAccess(node);
			}
			else if (node.Name == "Call")
			{
				return VisitCall(node);
			}
			else if (node.Name == "Dynamic")
			{
				return VisitDynamic(node);
			}
			else if (node.Name == "Symbol")
			{
				return VisitSymbol(node);
			}
			throw new NotImplementedException();
		}

		#region Scope Management

		private static void EnterScope()
		{
			_symbolScope.Push(new Dictionary<string, ParameterExpression>());
			_labelScope.Push(new Dictionary<string, LabelTarget>());
		}

		private static void LeaveScope()
		{
			_symbolScope.Pop();
			_labelScope.Pop();
		}

		private static Expression FindSymbol(string name)
		{
			foreach (var map in _symbolScope)
			{
				if (map.ContainsKey(name))
				{
					return map[name];
				}
			}
			return null;
		}

		private static Expression CreateSymbol(Type type, string name)
		{
			if (!Regex.Match(name, SYMBOL_FORMAT).Success)
			{
				throw new ArgumentException(string.Format("Symbol name is invalid, expected: {0}", SYMBOL_FORMAT), "name");
			}
			_symbolScope.Peek()[name] = Expression.Parameter(type, name);
			return _symbolScope.Peek()[name];
		}

		private static LabelTarget FindLabel(string name)
		{
			foreach (var map in _labelScope)
			{
				if (map.ContainsKey(name))
				{
					return map[name];
				}
			}
			return null;
		}

		private static LabelTarget CreateLabel(Type type, string name)
		{
			_labelScope.Peek()[name] = Expression.Label(type, name);
			return _labelScope.Peek()[name];
		}

		#endregion

		private static XmlNodeType PropertyType(XElement node, string propertyName)
		{
			if (node.Attribute(propertyName) != null)
			{
				return XmlNodeType.Attribute;
			}
			else if (node.Element(string.Format("{0}.{1}", node.Name, propertyName)) != null)
			{
				return XmlNodeType.Element;
			}
			return XmlNodeType.None;
		}

		private static LambdaExpression VisitLambda(XElement node)
		{
			EnterScope();
			var parameters = VisitArgumentList(node, "Parameters").Cast<ParameterExpression>().ToArray();
			var expr = Expression.Lambda(
				Translate(node.Element("Lambda.Body").Elements().Single()),
				parameters);
			LeaveScope();
			return expr;
		}

		private static Expression VisitParameter(XElement node)
		{
			var name = node.Attribute("Name").Value;
			var symbol = FindSymbol(name);
			if (symbol != null)
			{
				return symbol;
			}
			else
			{
				var type = TypeHelper.FindType((node.Attribute("Type") == null) ? "Object" : node.Attribute("Type").Value);
				return CreateSymbol(type, name);
			}
		}

		private static BlockExpression VisitBlock(XElement node)
		{
			EnterScope();
			var variables = VisitArgumentList(node, "Variables");
			var expr = Expression.Block(
				(variables != null) ? variables.Cast<ParameterExpression>().ToArray() : null,
				node.Element("Block.Expressions").Elements().Select(Translate));
			LeaveScope();
			return expr;
		}

		private static BinaryExpression VisitAssign(XElement node)
		{
			Expression left = null;
			switch (PropertyType(node, "Left"))
			{
				case XmlNodeType.Element:
					left = Translate(node.Element("Assign.Left").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					left = Evaluate(node.Attribute("Left").Value);
					break;
			}

			Expression right = null;
			switch (PropertyType(node, "Right"))
			{
				case XmlNodeType.Element:
					right = Translate(node.Element("Assign.Right").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					right = Evaluate(node.Attribute("Right").Value);
					break;
			}

			return Expression.Assign(left, right);
		}

		private static BinaryExpression VisitGreaterThan(XElement node)
		{
			Expression left = null;
			switch (PropertyType(node, "Left"))
			{
				case XmlNodeType.Element:
					left = Translate(node.Element("GreaterThan.Left").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					left = Evaluate(node.Attribute("Left").Value);
					break;
			}

			Expression right = null;
			switch (PropertyType(node, "Right"))
			{
				case XmlNodeType.Element:
					right = Translate(node.Element("GreaterThan.Right").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					right = Evaluate(node.Attribute("Right").Value);
					break;
			}

			return Expression.GreaterThan(left, right);
		}

		private static BinaryExpression VisitMultiplyAssign(XElement node)
		{
			Expression left = null;
			switch (PropertyType(node, "Left"))
			{
				case XmlNodeType.Element:
					left = Translate(node.Element("MultiplyAssign.Left").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					left = Evaluate(node.Attribute("Left").Value);
					break;
			}

			Expression right = null;
			switch (PropertyType(node, "Right"))
			{
				case XmlNodeType.Element:
					right = Translate(node.Element("MultiplyAssign.Right").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					right = Evaluate(node.Attribute("Right").Value);
					break;
			}

			return Expression.MultiplyAssign(left, right);
		}

		private static ConstantExpression VisitConstant(XElement node)
		{
			var type = TypeHelper.FindType(node.Attribute("Type").Value);
			return Expression.Constant(Convert.ChangeType(node.Attribute("Value").Value, type), type);
		}

		private static LoopExpression VisitLoop(XElement node)
		{
			EnterScope();
			var label = VisitLabelTarget(node.Element("Loop.BreakLabel").Elements().Single());
			var expr = Expression.Loop(
				Translate(node.Element("Loop.Body").Elements().Single()),
				label);
			LeaveScope();
			return expr;
		}

		private static LabelTarget VisitLabelTarget(XElement node)
		{
			var type = TypeHelper.FindType(node.Attribute("Type").Value);
			var name = node.Attribute("Name").Value;
			return VisitLabelTarget(name, type);
		}

		private static ConditionalExpression VisitConditional(XElement node)
		{
			return Expression.Condition(
				Translate(node.Element("Conditional.Test").Elements().Single()),
				Translate(node.Element("Conditional.IfTrue").Elements().Single()),
				Translate(node.Element("Conditional.IfFalse").Elements().Single()));
		}

		private static UnaryExpression VisitPostDecrementAssign(XElement node)
		{
			Expression operand = null;
			switch (PropertyType(node, "Operand"))
			{
				case XmlNodeType.Element:
					operand = Translate(node.Element("PostDecrementAssign.Operand").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					operand = Evaluate(node.Attribute("Operand").Value);
					break;
			}


			return Expression.PostDecrementAssign(operand);
		}

		private static GotoExpression VisitGoto(XElement node)
		{
			LabelTarget target = null;
			switch (PropertyType(node, "Target"))
			{
				case XmlNodeType.Element:
					target = VisitLabelTarget(node.Element("Goto.Target").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					target = VisitLabelTarget(node.Attribute("Target").Value);
					break;
			}

			Expression value = null;
			switch (PropertyType(node, "Value"))
			{
				case XmlNodeType.Element:
					value = Translate(node.Element("Goto.Value").Elements().Single());
					break;
				case XmlNodeType.Attribute:
					value = Evaluate(node.Attribute("Value").Value);
					break;
			}

			XAttribute typeAttr = node.Attribute("Type");
			Type type = null;
			if (typeAttr == null)
			{
				if (value != null)
				{
					type = value.Type;
				}
			}
			else
			{
				type = TypeHelper.FindType(typeAttr.Value);
			}

			return Expression.Goto(target, value, type ?? typeof(void));
		}

		private static MemberExpression VisitMemberAccess(XElement node)
		{
			return Expression.Property(Translate(node.Element("MemberAccess.Source").Elements().Single()), node.Attribute("Name").Value);
		}

		private static MethodCallExpression VisitCall(XElement node)
		{
			var methodName = node.Attribute("Name").Value;
			var arguments = VisitArgumentList(node, "Arguments").ToArray();
			var argumentTypes = arguments.Select(x => x.Type).ToArray();
			Expression instance = null;
			MethodInfo methodInfo = null;

			if (node.Element("Call.Object") != null)
			{
				instance = Translate(node.Element("Call.Object").Elements().Single());
				methodInfo = instance.Type.GetMethod(methodName, argumentTypes);
			}
			else
			{
				Type declaringType;
				if (node.Attribute("DeclaringType") != null)
				{
					declaringType = TypeHelper.FindType(node.Attribute("DeclaringType").Value);
				}
				else
				{
					var lastDot = methodName.LastIndexOf('.');
					declaringType = TypeHelper.FindType(methodName.Substring(0, lastDot));
					methodName = methodName.Substring(lastDot + 1, methodName.Length - lastDot - 1);
				}
				methodInfo = declaringType.GetMethod(methodName, argumentTypes);
			}

			return Expression.Call(instance, methodInfo, arguments);
		}

		private static IEnumerable<Type> VisitTypeList(XElement parentNode, string propertyName)
		{
			IEnumerable<string> typeNames;
			if (parentNode.Attribute(propertyName) != null)
			{
				typeNames = parentNode.Attribute(propertyName).Value.Split(',');
			}
			else
			{
				typeNames = parentNode.Element(string.Format("{0}.{1}", parentNode.Name.LocalName, propertyName)).Elements().Select(x => x.Value);
			}
			return typeNames.Select(TypeHelper.FindType);
		}

		private static Type VisitType(XElement node)
		{
			return TypeHelper.FindType(node.Value);
		}

		private static DynamicExpression VisitDynamic(XElement node)
		{
			var operation = (ExpressionType)Enum.Parse(typeof(ExpressionType), node.Attribute("Operation").Value);
			var type = TypeHelper.FindType((node.Attribute("Type") == null) ? "Object" : node.Attribute("Type").Value);
			var arguments = VisitArgumentList(node, "Arguments");

			var binder = (operation == ExpressionType.Invoke)
				? (CallSiteBinder)new SimpleInvokeBinder(new CallInfo(arguments.Count()))
				: new SimpleBinaryOperationBinder(operation);

			return Expression.Dynamic(binder, type, arguments);
		}

		private static IEnumerable<Expression> VisitArgumentList(XElement parentNode, string propertyName)
		{
			if (parentNode.Attribute(propertyName) != null)
			{
				return parentNode.Attribute(propertyName).Value.Split(',').Select(x => Evaluate(x));
			}
			else
			{
				return parentNode.Elements(string.Format("{0}.{1}", parentNode.Name.LocalName, propertyName)).Elements().Select(Translate);
			}
		}

		private static Expression VisitSymbol(XElement node)
		{
			var scope = VisitNodeProperty(node, "Scope");
			var symbolName = node.Attribute("Name").Value;
			return ExpressionEx.SymbolExpression(scope, symbolName);
		}

		private static Expression VisitNodeProperty(XElement node, string propertyName)
		{
			if (node.Attribute(propertyName) != null)
			{
				return Evaluate(node.Attribute(propertyName).Value);
			}
			else if (node.Element(string.Format("{0}.{1}", node.Name, propertyName)) != null)
			{
				return Translate(node.Element(string.Format("{0}.{1}", node.Name, propertyName)).Elements().Single());
			}
			return null;
		}

		private static Expression Evaluate(string text)
		{
			if ((text.First() != '{') && (text.Last() != '}'))
			{
				throw new ArgumentException("Text is not an inline expression.", "text");
			}
			text = text.Substring(1, text.Length - 2);

			// In order of precidence, make the expression either a string, number, or symbol.
			return VisitString(text) ?? VisitDouble(text) ?? VisitParameter(text);
		}

		private static Expression VisitString(string text)
		{
			if ((text.First() != '\'') && (text.Last() != '\''))
			{
				return null;
			}
			text = text.Substring(1, text.Length - 2);
			if (text.Length == 1)
			{
				return Expression.Constant(text[0], typeof(char));
			}
			else
			{
				return Expression.Constant(text, typeof(string));
			}
		}

		private static Expression VisitDouble(string text)
		{
			double result = 0;
			if (double.TryParse(text, out result))
			{
				return Expression.Constant(result, typeof(double));
			}
			return null;
		}

		private static Expression VisitParameter(string name)
		{
			var symbol = FindSymbol(name);
			if (symbol == null)
			{
				symbol = VisitInstanceVariable(name);
				if (symbol == null)
				{
					symbol = CreateSymbol(typeof(object), name);
				}
			}
			return symbol;
		}

		private static Expression VisitInstanceVariable(string name)
		{
			var symbols = name.Split('.');
			var valid = symbols.Select(x => Regex.Match(x, SYMBOL_FORMAT).Success).Aggregate(true, (complete, single) => complete && single);
			if (!valid || (symbols.Length <= 1))
			{
				return null;
			}

			Expression symbolExpr = VisitParameter(symbols[0]);
			for (int index = 1; index < symbols.Length; index++)
			{
				symbolExpr = ExpressionEx.SymbolExpression(symbolExpr, symbols[index]);
			}
			return symbolExpr;
		}

		private static LabelTarget VisitLabelTarget(string name, Type type = null)
		{
			return FindLabel(name) ?? CreateLabel(type ?? typeof(void), name);
		}
	}
}